package com.hy.video.monitor.rest;

import com.hy.video.monitor.config.AppProperties;
import com.hy.video.monitor.domain.Auth;
import com.hy.video.monitor.domain.MfaType;
import com.hy.video.monitor.domain.User;
import com.hy.video.monitor.domain.dto.LoginDto;
import com.hy.video.monitor.domain.dto.RegisterDto;
import com.hy.video.monitor.domain.dto.SendTotpDto;
import com.hy.video.monitor.domain.dto.TotpVerificationDto;
import com.hy.video.monitor.exception.*;
import com.hy.video.monitor.service.UserCacheService;
import com.hy.video.monitor.service.UserService;
import com.hy.video.monitor.service.email.EmailService;
import com.hy.video.monitor.service.sms.SmsService;
import com.hy.video.monitor.service.validation.UserValidationService;
import com.hy.video.monitor.exception.*;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.data.util.Pair;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

@Api(tags = "用户认证")
@CrossOrigin
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/authorize")
public class AuthorizeResource {

    private final UserService userService;
    private final UserCacheService userCacheService;
    private final SmsService smsService;
    private final EmailService emailService;
    private final AppProperties appProperties;
    private final UserValidationService userValidationService;

    @ApiOperation("判断用户名是否存在")
    @GetMapping("/validation/username")
    public boolean validateUsername(@RequestParam String username) {
        return userValidationService.isUsernameExisted(username);
    }

    @ApiOperation("判断电邮地址是否存在")
    @GetMapping("/validation/email")
    public boolean validateEmail(@RequestParam String email) {
        return userValidationService.isEmailExisted(email);
    }

    @ApiOperation("判断手机号是否存在")
    @GetMapping("/validation/mobile")
    public boolean validateMobile(@RequestParam String mobile) {
        return userValidationService.isMobileExisted(mobile);
    }


    @ApiOperation("注册一个新用户")
    @PostMapping("/register")
    public void register(@Valid @RequestBody RegisterDto registerDto) {
        userValidationService.validateUserUniqueFields(registerDto.getUsername(), registerDto.getEmail(), registerDto.getMobile());
        val user = User.builder()
            .username(registerDto.getUsername())
            .name(registerDto.getName())
            .email(registerDto.getEmail())
            .mobile(registerDto.getMobile())
            .password(registerDto.getPassword())
            .usingMfa(true)
            .enabled(false)
            .build();
        userService.register(user);
    }

    /**
     * 集成jwt登录或者TOTP(密码+验证码)
     * @param loginDTO
     * @return
     */
    @ApiOperation("用户登录")
    @PostMapping("/token")
    public ResponseEntity<?> login(@Valid @RequestBody LoginDto loginDTO) {
        // 1、校验用户名、密码
        return userService.findOptionalByUsernameAndPassword(loginDTO.getUsername(), loginDTO.getPassword())
            .map(user -> {
                // 2、升级密码编码,保存用户信息
                userService.upgradePasswordEncodingIfNeeded(user, loginDTO.getPassword());
                if (!user.isEnabled()) {
                    throw new UserNotEnabledProblem();
                }
                if (!user.isAccountNonLocked()) {
                    throw new UserAccountLockedProblem();
                }
                if (!user.isAccountNonExpired()) {
                    throw new UserAccountExpiredProblem();
                }
                // 不使用多因子认证
                // 3、判断 usingMfa 如果是 false，我们直接返回 Token
                if (!user.isUsingMfa()) {
                    return ResponseEntity.ok().body(userService.login(user));
                }
                /**
                 * 用户名、密码登录之后，这个时候服务端会给我们返回一个特殊的响应，要不然客户
                 * 端怎么知道这个用户需要二次验证。那么服务就会返回未授权的一个响应。但是为了
                 * 和用户名密码认证失败的响应区分开，我们在响应头中加入 X-Authenticate:mfa,realm=<请求id>
                 * 	请求id：第一次(用户名、密码)生成后，后端将请求id给存储到redis该用户记录中。验证码响应的
                 * 	时候带上该用户的登录请求的生成的请求id；完了第二次带上验证码和该请求id发送验证码请求，
                 * 	后端接收后一是判断验证码合法性，二是根据redis存储的该用户登录请求id和带过来的请求id是
                 * 	否是属于同一个用户请求做判断。判断完事后将结果返回给前端，这里所设计的是按照前后端分离
                 * 	项目验证码二次验证方案。
                 */
                // 4、使用多因子认证(验证码)
                val mfaId = userCacheService.cacheUser(user);
                return ResponseEntity
                    .status(HttpStatus.UNAUTHORIZED)
                    .header("X-Authenticate", "mfa", "realm=" + mfaId)
                    .build();
            })
            .orElseThrow(BadCredentialProblem::new);
    }

    /**
     * 刷新token-置换令牌
     * 刷新令牌开不了门，需要拿着刷新令牌访问接口刷一个令牌回来，
     * 它们使用不同的key签发的，你拿刷新令牌是没办法访问系统的。
     * @param authorization
     * @param refreshToken
     * @return 续命access_token
     */
    @ApiOperation("令牌刷新")
    @PostMapping("/token/refresh")
    public Auth refreshToken(@RequestHeader(name = "Authorization") String authorization, @RequestParam String refreshToken) {
        val accessToken = authorization.replace(appProperties.getJwt().getPrefix(), "");
        return userService.refreshToken(accessToken, refreshToken);
    }
    /**
     * 生成验证码并选择发送方式发送
     * @param sendTotpDto
     */
    @ApiOperation("生成验证码并选择发送方式发送")
    @PutMapping("/totp")
    public void sendTotp(@Valid @RequestBody SendTotpDto sendTotpDto) {
        // 前端第一次发完验证码成功候，没有输入验证码验证登录，等待时间超过cache中用户名密码登录时存放的mfdid有效期
        // 这里则返回empty，前端应该重新登录
        userCacheService.retrieveUser(sendTotpDto.getMfaId())
            // Pair.of 组成一对值很简便，不需要再编写一个实体类组装这一对值
            .flatMap(user -> userService.createTotp(user).map(code -> Pair.of(user, code)))
            // if else 函数式
            .ifPresentOrElse(pair -> {
                log.debug("totp: {}", pair.getSecond());
                if (sendTotpDto.getMfaType() == MfaType.SMS) {
                    smsService.send(pair.getFirst().getMobile(), pair.getSecond());
                }
//                else {
//                    emailService.send(pair.getFirst().getEmail(), pair.getSecond());
//                }
            }, () -> {
                throw new InvalidTotpProblem();
            });
    }

    /**
     * 校验码验证
     * @param totpVerificationDto
     * @return
     */
    @ApiOperation("校验验证码")
    @PostMapping("/totp")
    public Auth verifyTotp(@Valid @RequestBody TotpVerificationDto totpVerificationDto) {
        return userCacheService.verifyTotp(totpVerificationDto.getMfaId(), totpVerificationDto.getCode())
            .map(User::getUsername)
            .flatMap(userService::findOptionalByUsername)
            .map(userService::loginWithTotp)
            .orElseThrow(InvalidTotpProblem::new);
    }
}
